import defined from '../Core/defined.js';
import ShaderSource from '../Renderer/ShaderSource.js';

    /**
     * @private
     */
    function ShadowMapShader() {
    }

    ShadowMapShader.getShadowCastShaderKeyword = function(isPointLight, isTerrain, usesDepthTexture, isOpaque) {
        return 'castShadow ' + isPointLight + ' ' + isTerrain + ' ' + usesDepthTexture + ' ' + isOpaque;
    };

    ShadowMapShader.createShadowCastVertexShader = function(vs, isPointLight, isTerrain) {
        var defines = vs.defines.slice(0);
        var sources = vs.sources.slice(0);

        defines.push('SHADOW_MAP');

        if (isTerrain) {
            defines.push('GENERATE_POSITION');
        }

        var positionVaryingName = ShaderSource.findPositionVarying(vs);
        var hasPositionVarying = defined(positionVaryingName);

        if (isPointLight && !hasPositionVarying) {
            var length = sources.length;
            for (var j = 0; j < length; ++j) {
                sources[j] = ShaderSource.replaceMain(sources[j], 'czm_shadow_cast_main');
            }

            var shadowVS =
                'varying vec3 v_positionEC; \n' +
                'void main() \n' +
                '{ \n' +
                '    czm_shadow_cast_main(); \n' +
                '    v_positionEC = (czm_inverseProjection * gl_Position).xyz; \n' +
                '}';
            sources.push(shadowVS);
        }

        return new ShaderSource({
            defines : defines,
            sources : sources
        });
    };

    ShadowMapShader.createShadowCastFragmentShader = function(fs, isPointLight, usesDepthTexture, opaque) {
        var defines = fs.defines.slice(0);
        var sources = fs.sources.slice(0);

        var positionVaryingName = ShaderSource.findPositionVarying(fs);
        var hasPositionVarying = defined(positionVaryingName);
        if (!hasPositionVarying) {
            positionVaryingName = 'v_positionEC';
        }

        var length = sources.length;
        for (var i = 0; i < length; ++i) {
            sources[i] = ShaderSource.replaceMain(sources[i], 'czm_shadow_cast_main');
        }

        var fsSource = '';

        if (isPointLight) {
            if (!hasPositionVarying) {
                fsSource += 'varying vec3 v_positionEC; \n';
            }
            fsSource += 'uniform vec4 shadowMap_lightPositionEC; \n';
        }

        if (opaque) {
            fsSource +=
                'void main() \n' +
                '{ \n';
        } else {
            fsSource +=
                'void main() \n' +
                '{ \n' +
                '    czm_shadow_cast_main(); \n' +
                '    if (gl_FragColor.a == 0.0) \n' +
                '    { \n' +
                '       discard; \n' +
                '    } \n';
        }

        if (isPointLight) {
            fsSource +=
                '    float distance = length(' + positionVaryingName + '); \n' +
                '    if (distance >= shadowMap_lightPositionEC.w) \n' +
                '    { \n' +
                '        discard; \n' +
                '    } \n' +
                '    distance /= shadowMap_lightPositionEC.w; // radius \n' +
                '    gl_FragColor = czm_packDepth(distance); \n';
        } else if (usesDepthTexture) {
            fsSource += '    gl_FragColor = vec4(1.0); \n';
        } else {
            fsSource += '    gl_FragColor = czm_packDepth(gl_FragCoord.z); \n';
        }

        fsSource += '} \n';

        sources.push(fsSource);

        return new ShaderSource({
            defines : defines,
            sources : sources
        });
    };

    ShadowMapShader.getShadowReceiveShaderKeyword = function(shadowMap, castShadows, isTerrain, hasTerrainNormal) {
        var usesDepthTexture = shadowMap._usesDepthTexture;
        var polygonOffsetSupported = shadowMap._polygonOffsetSupported;
        var isPointLight = shadowMap._isPointLight;
        var isSpotLight = shadowMap._isSpotLight;
        var hasCascades = shadowMap._numberOfCascades > 1;
        var debugCascadeColors = shadowMap.debugCascadeColors;
        var softShadows = shadowMap.softShadows;

        return 'receiveShadow ' + usesDepthTexture + polygonOffsetSupported + isPointLight + isSpotLight +
               hasCascades + debugCascadeColors + softShadows + castShadows + isTerrain + hasTerrainNormal;
    };

    ShadowMapShader.createShadowReceiveVertexShader = function(vs, isTerrain, hasTerrainNormal) {
        var defines = vs.defines.slice(0);
        var sources = vs.sources.slice(0);

        defines.push('SHADOW_MAP');

        if (isTerrain) {
            if (hasTerrainNormal) {
                defines.push('GENERATE_POSITION_AND_NORMAL');
            } else {
                defines.push('GENERATE_POSITION');
            }
        }

        return new ShaderSource({
            defines : defines,
            sources : sources
        });
    };

    ShadowMapShader.createShadowReceiveFragmentShader = function(fs, shadowMap, castShadows, isTerrain, hasTerrainNormal) {
        var normalVaryingName = ShaderSource.findNormalVarying(fs);
        var hasNormalVarying = (!isTerrain && defined(normalVaryingName)) || (isTerrain && hasTerrainNormal);

        var positionVaryingName = ShaderSource.findPositionVarying(fs);
        var hasPositionVarying = defined(positionVaryingName);

        var usesDepthTexture = shadowMap._usesDepthTexture;
        var polygonOffsetSupported = shadowMap._polygonOffsetSupported;
        var isPointLight = shadowMap._isPointLight;
        var isSpotLight = shadowMap._isSpotLight;
        var hasCascades = shadowMap._numberOfCascades > 1;
        var debugCascadeColors = shadowMap.debugCascadeColors;
        var softShadows = shadowMap.softShadows;
        var bias = isPointLight ? shadowMap._pointBias : (isTerrain ? shadowMap._terrainBias : shadowMap._primitiveBias);

        var defines = fs.defines.slice(0);
        var sources = fs.sources.slice(0);

        var length = sources.length;
        for (var i = 0; i < length; ++i) {
            sources[i] = ShaderSource.replaceMain(sources[i], 'czm_shadow_receive_main');
        }

        if (isPointLight) {
            defines.push('USE_CUBE_MAP_SHADOW');
        } else if (usesDepthTexture) {
            defines.push('USE_SHADOW_DEPTH_TEXTURE');
        }

        if (softShadows && !isPointLight) {
            defines.push('USE_SOFT_SHADOWS');
        }

        // Enable day-night shading so that the globe is dark when the light is below the horizon
        if (hasCascades && castShadows && isTerrain) {
            if (hasNormalVarying) {
                defines.push('ENABLE_VERTEX_LIGHTING');
            } else {
                defines.push('ENABLE_DAYNIGHT_SHADING');
            }
        }

        if (castShadows && bias.normalShading && hasNormalVarying) {
            defines.push('USE_NORMAL_SHADING');
            if (bias.normalShadingSmooth > 0.0) {
                defines.push('USE_NORMAL_SHADING_SMOOTH');
            }
        }

        var fsSource = '';

        if (isPointLight) {
            fsSource += 'uniform samplerCube shadowMap_textureCube; \n';
        } else {
            fsSource += 'uniform sampler2D shadowMap_texture; \n';
        }

        var returnPositionEC;
        if (hasPositionVarying) {
            returnPositionEC = '    return vec4(' + positionVaryingName + ', 1.0); \n';
        } else {
            returnPositionEC =
                '#ifndef LOG_DEPTH \n' +
                '    return czm_windowToEyeCoordinates(gl_FragCoord); \n' +
                '#else \n' +
                '    return vec4(v_logPositionEC, 1.0); \n' +
                '#endif \n';
        }

        fsSource +=
            'uniform mat4 shadowMap_matrix; \n' +
            'uniform vec3 shadowMap_lightDirectionEC; \n' +
            'uniform vec4 shadowMap_lightPositionEC; \n' +
            'uniform vec4 shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness; \n' +
            'uniform vec4 shadowMap_texelSizeDepthBiasAndNormalShadingSmooth; \n' +
            '#ifdef LOG_DEPTH \n' +
            'varying vec3 v_logPositionEC; \n' +
            '#endif \n' +
            'vec4 getPositionEC() \n' +
            '{ \n' +
            returnPositionEC +
            '} \n' +
            'vec3 getNormalEC() \n' +
            '{ \n' +
            (hasNormalVarying ?
            '    return normalize(' + normalVaryingName + '); \n' :
            '    return vec3(1.0); \n') +
            '} \n' +

            // Offset the shadow position in the direction of the normal for perpendicular and back faces
            'void applyNormalOffset(inout vec4 positionEC, vec3 normalEC, float nDotL) \n' +
            '{ \n' +
            (bias.normalOffset && hasNormalVarying ?
            '    float normalOffset = shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness.x; \n' +
            '    float normalOffsetScale = 1.0 - nDotL; \n' +
            '    vec3 offset = normalOffset * normalOffsetScale * normalEC; \n' +
            '    positionEC.xyz += offset; \n' : '') +
            '} \n';

        fsSource +=
            'void main() \n' +
            '{ \n' +
            '    czm_shadow_receive_main(); \n' +
            '    vec4 positionEC = getPositionEC(); \n' +
            '    vec3 normalEC = getNormalEC(); \n' +
            '    float depth = -positionEC.z; \n';

        fsSource +=
            '    czm_shadowParameters shadowParameters; \n' +
            '    shadowParameters.texelStepSize = shadowMap_texelSizeDepthBiasAndNormalShadingSmooth.xy; \n' +
            '    shadowParameters.depthBias = shadowMap_texelSizeDepthBiasAndNormalShadingSmooth.z; \n' +
            '    shadowParameters.normalShadingSmooth = shadowMap_texelSizeDepthBiasAndNormalShadingSmooth.w; \n' +
            '    shadowParameters.darkness = shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness.w; \n';

        if (isTerrain) {
            // Scale depth bias based on view distance to reduce z-fighting in distant terrain
            fsSource += '    shadowParameters.depthBias *= max(depth * 0.01, 1.0); \n';
        } else if (!polygonOffsetSupported) {
            // If polygon offset isn't supported push the depth back based on view, however this
            // causes light leaking at further away views
            fsSource += '    shadowParameters.depthBias *= mix(1.0, 100.0, depth * 0.0015); \n';
        }

        if (isPointLight) {
            fsSource +=
                '    vec3 directionEC = positionEC.xyz - shadowMap_lightPositionEC.xyz; \n' +
                '    float distance = length(directionEC); \n' +
                '    directionEC = normalize(directionEC); \n' +
                '    float radius = shadowMap_lightPositionEC.w; \n' +
                '    // Stop early if the fragment is beyond the point light radius \n' +
                '    if (distance > radius) \n' +
                '    { \n' +
                '        return; \n' +
                '    } \n' +
                '    vec3 directionWC  = czm_inverseViewRotation * directionEC; \n' +

                '    shadowParameters.depth = distance / radius; \n' +
                '    shadowParameters.nDotL = clamp(dot(normalEC, -directionEC), 0.0, 1.0); \n' +

                '    shadowParameters.texCoords = directionWC; \n' +
                '    float visibility = czm_shadowVisibility(shadowMap_textureCube, shadowParameters); \n';
        } else if (isSpotLight) {
            fsSource +=
                '    vec3 directionEC = normalize(positionEC.xyz - shadowMap_lightPositionEC.xyz); \n' +
                '    float nDotL = clamp(dot(normalEC, -directionEC), 0.0, 1.0); \n' +
                '    applyNormalOffset(positionEC, normalEC, nDotL); \n' +

                '    vec4 shadowPosition = shadowMap_matrix * positionEC; \n' +
                '    // Spot light uses a perspective projection, so perform the perspective divide \n' +
                '    shadowPosition /= shadowPosition.w; \n' +

                '    // Stop early if the fragment is not in the shadow bounds \n' +
                '    if (any(lessThan(shadowPosition.xyz, vec3(0.0))) || any(greaterThan(shadowPosition.xyz, vec3(1.0)))) \n' +
                '    { \n' +
                '        return; \n' +
                '    } \n' +

                '    shadowParameters.texCoords = shadowPosition.xy; \n' +
                '    shadowParameters.depth = shadowPosition.z; \n' +
                '    shadowParameters.nDotL = nDotL; \n' +

                '    float visibility = czm_shadowVisibility(shadowMap_texture, shadowParameters); \n';
        } else if (hasCascades) {
            fsSource +=
                '    float maxDepth = shadowMap_cascadeSplits[1].w; \n' +

                '    // Stop early if the eye depth exceeds the last cascade \n' +
                '    if (depth > maxDepth) \n' +
                '    { \n' +
                '        return; \n' +
                '    } \n' +

                '    // Get the cascade based on the eye-space depth \n' +
                '    vec4 weights = czm_cascadeWeights(depth); \n' +

                '    // Apply normal offset \n' +
                '    float nDotL = clamp(dot(normalEC, shadowMap_lightDirectionEC), 0.0, 1.0); \n' +
                '    applyNormalOffset(positionEC, normalEC, nDotL); \n' +

                '    // Transform position into the cascade \n' +
                '    vec4 shadowPosition = czm_cascadeMatrix(weights) * positionEC; \n' +

                '    // Get visibility \n' +
                '    shadowParameters.texCoords = shadowPosition.xy; \n' +
                '    shadowParameters.depth = shadowPosition.z; \n' +
                '    shadowParameters.nDotL = nDotL; \n' +
                '    float visibility = czm_shadowVisibility(shadowMap_texture, shadowParameters); \n' +

                '    // Fade out shadows that are far away \n' +
                '    float shadowMapMaximumDistance = shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness.z; \n' +
                '    float fade = max((depth - shadowMapMaximumDistance * 0.8) / (shadowMapMaximumDistance * 0.2), 0.0); \n' +
                '    visibility = mix(visibility, 1.0, fade); \n' +

                (debugCascadeColors ?
                '    // Draw cascade colors for debugging \n' +
                '    gl_FragColor *= czm_cascadeColor(weights); \n' : '');
        } else {
            fsSource +=
                '    float nDotL = clamp(dot(normalEC, shadowMap_lightDirectionEC), 0.0, 1.0); \n' +
                '    applyNormalOffset(positionEC, normalEC, nDotL); \n' +
                '    vec4 shadowPosition = shadowMap_matrix * positionEC; \n' +

                '    // Stop early if the fragment is not in the shadow bounds \n' +
                '    if (any(lessThan(shadowPosition.xyz, vec3(0.0))) || any(greaterThan(shadowPosition.xyz, vec3(1.0)))) \n' +
                '    { \n' +
                '        return; \n' +
                '    } \n' +

                '    shadowParameters.texCoords = shadowPosition.xy; \n' +
                '    shadowParameters.depth = shadowPosition.z; \n' +
                '    shadowParameters.nDotL = nDotL; \n' +
                '    float visibility = czm_shadowVisibility(shadowMap_texture, shadowParameters); \n';
        }

        fsSource +=
            '    gl_FragColor.rgb *= visibility; \n' +
            '} \n';

        sources.push(fsSource);

        return new ShaderSource({
            defines : defines,
            sources : sources
        });
    };
export default ShadowMapShader;
